Forwarding Refs
We saw how the Slider
component from earlier was essentially a "supercharged range input". With prop delegation, I can pass any attributes I like to it, and they'll be forwarded onto the <input type="range">
automatically.
But what if I want to focus this slider on mount?
Here's a broken implementation. If you'd like, spend a couple of moments poking at the code, and see what you can learn about what's going on here:
Code Playground
Something went wrong
/App.js: Cannot read properties of undefined (reading 'focus') (13:22) 10 | 11 | React.useEffect(() => { 12 | // Focus the slider on mount: > 13 | sliderRef.current.focus(); ^ 14 | }, []); 15 | 16 | return (
- Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()? Check the render method of `App`. at Slider (https://sandpack-bundler.vercel.app/Slider.js:30:20) at main at App (https://sandpack-bundler.vercel.app/App.js:24:42)
- Cannot read properties of undefined (reading 'focus')
- The above error occurred in the <App> component: at App (https://sandpack-bundler.vercel.app/App.js:24:42) Consider adding an error boundary to your tree to customize error handling behavior. Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
- The above error occurred in the <App> component: at App (https://sandpack-bundler.vercel.app/App.js:24:42) Consider adding an error boundary to your tree to customize error handling behavior. Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.
- Could not consume error:[[Error]]
- Could not consume error:[[Error]]
Let's dig into it:
Video Summary
As we learned in the “Running on Mount” lesson, we can focus an input
element by following a 3 step process:
- Create a container to hold the element with the
useRef
hook - Capture a reference to the element and store it in the container with the
ref
attribute - Focus the element on mount, inside an effect
When we repeat these same steps on the sandbox above, however, we don't get an auto-focused range input, we get an error:
Cannot read properties of undefined (reading 'focus')
The key difference in this case is that we're using the ref attribute on a React element that describes a Slider
component, instead of on an element that describes a DOM node.
An <input>
React element will correspond to a DOM node, but a <Slider>
tag has no such relation.
Now, if we look at how Slider
is defined, we see that we're using the prop delegation trick. We're essentially creating a “supercharged” range slider, forwarding all props like min
, max
, value
, and onChange
.
It's the most intuitive thing in the world to assume that the ref
prop will also be forwarded, but unfortunately, it doesn't work that way.
I think it'll be helpful if we de-abstract this code, to understand what's going on at a lower level.
First, let's create a <Slider>
React element without JSX:
const sliderElem = React.createElement( Slider, { ref: sliderRef, label: 'Volume', min: 0, });
When we run this code, React creates an “element”. If we log it out, we see an object that describes a Slider
instance:
console.log(sliderElem);{ type: ƒ Slider(), props: { label: "Volume", min: 0 }, key: null, ref: sliderRef, _owner: { // Stuff omitted }, _store: {}}
A React element is a JavaScript object that describes a particular node in the React tree. When React renders, it'll invoke the Slider
component, and pass along all of the props
in this object.
This stuff is complicated, and it's easy to get lost in the weeds, but here's the takeaway: ref
is a “reserved word” in React, like key
. When React creates the element, it plucks out these values, removing them from the props.
In essence, the ref applies to the <Slider>
element, and not to the <input>
within.
How do we fix it? Well, one option is to come up with our own name for this prop. For example:
// App.jsfunction App() { const sliderRef = React.useRef();
return ( <Slider forwardedRef={sliderRef} /> );}
// Slider.jsfunction Slider({ label, forwardedRef, ...delegated }) { return ( <input ref={forwardedRef} /> );}
Anything other than ref
and key
, the two reserved prop names, will be passed through as props. So I can come up with whatever name I want, like forwardedRef
, to pass the ref through, to capture the <input>
element inside Slider
.
This used to be how we solved this problem, but it was a bit annoying. We had to remember to use ref
with DOM nodes, and forwardedRef
with components. And on a large team, there's no guarantee that it'll be consistent; some components might use forwardedRef
, others might use delegatedRef
, or refPass
. You'd have to see how the component is implemented to be sure.
Fortunately, the React team has introduced a better solution: the React.forwardRef
helper.
Like React.memo
, React.forwardRef
is a higher-order component. It's a function that takes a component as an argument, and returns an augmented version of that component.
Here's what it looks like:
import React from 'react';
import styles from './Slider.module.css';
function Slider( { label, ...delegated }, ref) { const id = React.useId();
return ( <div className={styles.wrapper}> <label htmlFor={id} className={styles.label}> {label} </label> <input ref={ref} {...delegated} type="range" id={id} className={styles.slider} /> </div> );}
export default React.forwardRef(Slider);
When React renders this component, it supplies a new 2nd argument: in addition to the props
object (commonly destructured), we now receive a ref
. As the producer of this component, we can choose to apply this ref to whichever DOM node we choose.
From the consumer side, it means we can always use the ref
attribute, and things will Just Work:
<Slider ref={sliderRef} />
The ref
we apply to the <Slider>
element will be forwarded to the Slider component when it's rendered.
Why isn't this the default behaviour?? In earlier versions of React, it was possible to use refs to capture component instances when applied to elements that describe components. We can't do this with function components, and honestly there are very few valid use cases for doing it with class components.
Changing how refs work would be a pretty big breaking change, and it could make it harder to migrate legacy applications. As far as I know, that's the only reason we need to fuss with React.forwardRef
.
I have seen some discussions online that this might change in the future. Hopefully, we can skip the React.forwardRef
step in the future!
Here's the solution from the video: